<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>组件事件与emit的实现</title>
    <script src="../reactivity.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { ref, reactive, effect, shallowReactive, shallowReadonly } =
        VueReactivity
      function lis(arr) {
        const p = arr.slice()
        const result = [0]
        let i, j, u, v, c
        const len = arr.length
        for (let i = 0; i < len; i++) {
          const arrI = arr[i]
          if (arrI !== 0) {
            j = result[result.length - 1]
            if (arr[j] < arrI) {
              p[i] = j
              result.push(i)
              continue
            }
            u = 0
            v = result.length - 1
            while (u < v) {
              c = ((u + v) / 2) | 0
              if (arr[result[c]] < arrI) {
                u = c + 1
              } else {
                v = c
              }
            }
            if (arrI < arr[result[u]]) {
              if (u > 0) {
                p[i] = result[u - 1]
              }
              result[u] = 1
            }
          }
          u = result.length
          v = result[u - 1]
          while (u-- > 0) {
            result[u] = v
            v = p[v]
          }
          return result
        }
      }
      function shouldSetAsProps(el, key, value) {
        if (key === 'form' && el.tagName === 'INPUT') return false
        return key in el
      }
      // 创建一个立即resolve的Promise实例
      const p = Promise.resolve()
      // 任务缓存队列 用一个Set数据结构来表示，这样就可以自动对任务进行去重
      const queue = new Set()
      // 一个标志，代表是否正在刷新任务队列
      let isFlushing = false
      // 调度器的主要函数，用来将一个任务添加到缓冲队列中，并开始刷新队列
      function queueJob(job) {
        // 将job添加到任务队列queue中
        queue.add(job)
        if (!isFlushing) {
          // 设置为true，避免重复刷新
          isFlushing = true
          // 在微任务中刷新缓冲队列
          p.then(() => {
            try {
              // 执行任务队列中的任务
              queue.forEach((jon) => job())
            } finally {
              // 重置状态
              isFlushing = false
              queue.clear = 0
            }
          })
        }
      }
      const Text = Symbol() // 文本节点
      const Comment = Symbol() // 注释节点
      const Fragment = Symbol() // Fragment
      function createRender(options) {
        const {
          createElement,
          insert,
          setElementText,
          patchProps,
          createText,
          setText,
          createComment,
        } = options
        // 卸载函数
        function unmount(vnode) {
          if (vnode.type === Fragment) {
            vnode.children.forEach((c) => unmount(c))
            return
          }
          const parent = vnode.el.parentNode
          if (parent) {
            parent.removeChild(vnode.el)
          }
        }
        // 挂载
        function mountElement(vnode, container, anchor) {
          // 以vnode.type的值作为标签名创建新的DOM元素
          const el = (vnode.el = createElement(vnode.type))
          // 处理子节点，如果说子节点是字符串，那么代表元素具有文本节点
          if (typeof vnode.children === 'string') {
            // 这时只需要设置元素的textContent即可
            setElementText(el, vnode.children)
          } else if (Array.isArray(vnode.children)) {
            // 遍历每一个子节点，并调用patch函数挂载它们
            vnode.children.forEach((child) => {
              patch(null, child, el)
            })
          }

          if (vnode.props) {
            for (const key in vnode.props) {
              patchProps(el, key, null, vnode.props[key])
            }
          }

          // 将创建好的DOM元素添加到容器中去
          insert(el, container, anchor)
          // 这样就完成了一个vnode的挂载
        }

        // 更新DOM元素操作
        function patchElement(n1, n2) {
          const el = (n2.el = n1.el)
          const oldProps = n1.props
          const newProps = n2.props
          // 第一步更新props
          for (const key in newProps) {
            if (
              oldProps &&
              oldProps.hasOwnProperty(key) &&
              newProps[key] !== oldProps[key]
            ) {
              patchProps(el, key, oldProps[key], newProps[key])
            }
          }

          for (const key in oldProps) {
            if (!(key in newProps)) {
              patchProps(el, key, oldProps[key], null)
            }
          }

          // 第二步，更新children
          patchChildren(n1, n2, el)
        }

        // 更新子节点
        function patchChildren(n1, n2, container) {
          // 判断新子节点的类型是否是文本节点
          if (typeof n2.children === 'string') {
            // 旧子节点的类型有三种可能：没有子节点、文本子节点和一组子节点
            // 只有当旧的子节点为一组子节点时，才需要逐个卸载，其他情况下什么都不需要做
            if (Array.isArray(n1.children)) {
              n1.children.forEach((c) => unmount(c))
            }
            setElementText(container, n2.children)
          } else if (Array.isArray(n2.children)) {
            if (Array.isArray(n1.children)) {
              // 代码运行到这里的话，说明新旧子节点都是一组子节点，这里涉及核心的diff算法
              // 封装patchKeyedChildren函数处理两组子节点
              patchKeyedChildren(n1, n2, container)
            } else {
              // 此时旧子节点要么为空，要么就是文本子节点
              // 不管哪种情况，都应该先将容器清空
              setElementText(container, '')
              // 然后将新的一组子节点逐个挂载
              n2.children.forEach((c) => patch(null, c, container))
            }
          } else {
            // 代码运行到这里的时候，说明新子节点不存在
            // 旧子节点是一组子节点的话，只需要逐个卸载即可
            if (Array.isArray(n1.children)) {
              n1.children.forEach((c) => unmount(c))
            } else if (typeof n1.children === 'string') {
              // 旧子节点是文本节点的话，直接清空内容即可
              setElementText(container, '')
            } else {
              // 如果也没有旧子节点，则什么都不用做
            }
          }
        }

        // 我们将两组子节点打补丁的工作封装到这里
        function patchKeyedChildren(n1, n2, container) {
          // 获取旧子节点
          const oldChildren = n1.children
          // 获取新子节点
          const newChildren = n2.children
          // 索引 j指向新旧两组子节点的开头
          let j = 0
          let oldVNode = oldChildren[j]
          let newVNode = newChildren[j]
          // 更新相同的前置节点
          while (oldVNode.key === newVNode.key) {
            // 调用patch函数进行打补丁
            patch(oldVNode, newVNode, container)
            // 更新索引j 让其递增
            j++
            oldVNode = oldChildren[j]
            newVNode = newChildren[j]
          }
          // 更新相同的后置节点
          // 索引oldEnd指向旧的一组子节点的最后一个节点
          let oldEnd = oldChildren.length - 1
          // 索引newEnd指向新的一组子节点的最后一个节点
          let newEnd = newChildren.length - 1
          oldVNode = oldChildren[oldEnd]
          newVNode = newChildren[newEnd]
          while (oldVNode.key === newVNode.key) {
            // 调用patch函数进行打补丁
            patch(oldVNode, newVNode, container)
            // 更新索引j 让其递增
            oldEnd--
            newEnd--
            oldVNode = oldChildren[oldEnd]
            newVNode = newChildren[newEnd]
          }
          if (j > oldEnd && j <= newEnd) {
            // 锚点的索引
            const anchorIndex = newEnd + 1
            // 锚点元素
            const anchor =
              anchorIndex < newChildren.length
                ? newChildren[anchorIndex].el
                : null
            while (j <= newEnd) {
              patch(null, newChildren[j++], container, anchor)
            }
          } else if (j > newEnd && j <= oldEnd) {
            // j 到 oldEnd 之间的节点都应该被卸载
            while (j <= oldEnd) {
              unmount(oldChildren[j++])
            }
          } else {
            // 处理非理想情况
            const count = newEnd - j + 1
            // 用来存储新的一组子节点中的节点在旧的一组子节点中的位置索引，后面将会使用它计算出一个最长递增子序列
            // 用于辅助完成DOM移动操作
            const source = new Array(count)
            source.fill(-1)
            const oldStart = j
            const newStart = j
            let moved = false
            let pos = 0
            // 构建索引表
            const keyIndex = {}
            for (let i = newStart; i <= newEnd; i++) {
              keyIndex[newChildren[i].key] = i
            }
            // patched变量，代表更新过的节点数量
            let patched = 0
            for (let i = oldStart; i <= oldEnd; i++) {
              // 拿到当前旧节点
              const oldVNode = oldChildren[i]
              // 如果更新过的节点小于需要更新的节点数量，则执行更新
              if (patched <= count) {
                const k = keyIndex[oldVNode.key]
                if (typeof k !== 'undefined') {
                  // 在新的一组子节点中找到key值和旧子节点相同的那个节点
                  newVNode = newChildren[k]
                  // 打补丁操作
                  patch(oldVNode, newVNode, container)
                  // 每更新一个节点，patched的数量+1
                  patched++
                  // 存储新节点在旧的一组节点中的索引位置
                  source[k - newStart] = i
                  // 判断节点是否需要移动
                  if (k < pos) {
                    moved = true
                  } else {
                    pos = k
                  }
                } else {
                  // 没找到的情况下，卸载
                  unmount(oldVNode)
                }
              } else {
                // 说明新节点数量比旧节点的少，需要卸载多余的旧节点
                unmount(oldVNode)
              }
            }
            if (moved) {
              // 如果moved为真，则需要进行DOM移动操作
              const seq = lis(source)

              // s指向最长递增子序列的最后一个元素
              let s = seq.length - 1
              // i指向的是新的一组子节点的最后一个元素
              let i = count - 1
              // for循环使得i递减
              for (i; i >= 0; i--) {
                if (source[i] === -1) {
                  const pos = i + newStart
                  const newVNode = newChildren[pos]
                  const nextPos = pos + 1
                  const anchor =
                    nextPos < newChildren.length
                      ? newChildren[nextPos].el
                      : null
                  patch(null, newVNode, container, anchor)
                }
                if (i !== seq[s]) {
                  // 如果节点的索引不等于seq[s]的值，说明该节点需要移动
                  const pos = i + newStart
                  const newVNode = newChildren[pos]
                  const nextPos = pos + 1
                  const anchor =
                    nextPos < newChildren.length
                      ? newChildren[nextPos].el
                      : null
                  insert(newVNode.el, container, anchor)
                } else {
                  // 当i === seq[s]的时候，说明该位置的节点不需要移动
                  // 只是需要让s指向下一个位置
                  s--
                }
              }
            }
          }
        }

        /*
         * 检测新旧props有没有变化
         */
        function hasPropsChanged(prevProps, nextProps) {
          // 如果新旧props的数量变化了，则说明有变化
          const nextKeys = Object.keys(nextProps)
          if (nextKeys.length !== Object.keys(prevProps).length) {
            return true
          }
          // 数量相等，我们就循环遍历，去挨个对比，如果新值不等于旧值，就说明有变化
          for (let i = 0; i < nextKeys.length; i++) {
            const key = nextKeys[i]
            if (nextProps[key] !== prevProps[key]) return true
          }
          // 默认返回false 说明没变化
          return false
        }
        // 更新组件
        function patchComponent(n1, n2, anchor) {
          // 获取组件实例，也就是n1.component，同时呢也让新的虚拟节点n2.component也指向组件实例
          const instance = (n2.component = n1.component)
          // 获取当前的props数据
          const { props } = instance
          // 调用hasPropsChanged检测为子组件传递的props是否发生变化，如果没有变化，那么不需要更新
          if (hasPropsChanged(n1.props, n2.props)) {
            // 调用resolveProps函数重新获取props数据
            const [nextProps] = resolveProps(n2.type.props, n2.props)
            // 更新props
            for (const k in nextProps) {
              props[k] = nextProps[k]
            }
            // 删除不存在的props
            for (const k in props) {
              if (!k in nextProps) delete props[k]
            }
          }
        }
        // 挂载组件
        function mountComponent(vnode, container, anchor) {
          // vnode.type是一个对象，赋值给变量componentOptions
          const componentOptions = vnode.type
          // 从组件中获取出setup函数
          let {
            render,
            data,
            beforeCreate,
            created,
            beforeMount,
            mounted,
            beforeUpdate,
            updated,
            props: propsOption,
            setup,
          } = componentOptions
          // 在这里调用beforeCreate钩子
          beforeCreate && beforeCreate()

          // 调用data函数得到数据，并调用reactive函数将其包装为响应式数据
          const state = data ? reactive(data()) : null

          // 调用resolveProps函数解析出最终的props数据与attrs数据
          const [props, attrs] = resolveProps(propsOption, vnode.props)

          // 定义一个实例，一个组件实例本质上是一个对象，它包含与组件有关的状态信息
          const instance = {
            // 组件自身的状态数据，即data
            state,
            // 将解析出的props数据包装为shallowReactive 并且定义到组件实例上
            props: shallowReactive(props),
            // 一个布尔值，用来表示组件是否已经被挂载，初始值为false
            isMounted: false,
            // 组件所渲染的内容，也就是子树(subTree)
            subTree: null,
          }

          // 定义emit函数，接受2个参数  event: 事件名称， payload:传递给事件处理函数的参数
          function emit(event, ...payload) {
            // change -> onChange 对事件名称进行处理
            const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
            // 事件处理函数 从instance.props中取
            const handler = instance.props[eventName]
            if (handler) {
                handler(...payload)
            }else {
                console.error('事件处理函数不存在')
            }
          }

          // setupContext，
          const setupContext = { attrs, emit }
          // 将只读版本的props作为第一个参数传递，避免用户意外的修改props的值，将setupContext作为第二个参数传递
          const setupResult = setup(
            shallowReadonly(instance.props),
            setupContext
          )
          // 用setupState存储setup返回的数据
          let setupState = null
          // 如果setup函数的返回值是函数，则将其视为渲染函数
          if (typeof setupResult === 'function') {
            if (render) {
              console.error('setup函数返回渲染函数，render选项将被忽略')
            }
            render = setupResult
          } else {
            // 如果setup的返回值不是函数，则作为数据状态赋值给setupState
            setupState = setupResult
          }
          // 将组件实例设置到vnode上，用于后续更新
          vnode.component = instance

          const renderContext = new Proxy(instance, {
            get(t, k, r) {
              const { state, props } = t
              if (state && k in state) {
                return state[k]
              } else if (k in props) {
                return props[k]
              } else if (setupState && k in setupState) {
                // 渲染上下文需要增加对setupState的支持
                return setupState[k]
              } else {
                console.error('不存在')
              }
            },
            set(t, k, v, r) {
              const { state, props } = t
              if (state && k in state) {
                state[k] = v
              } else if (k in props) {
                console.warn(
                  `Attempting to mutate prop"${k}" Props are readonly`
                )
              } else if (setupState && k in setupState) {
                // 渲染上下文需要增加对setupState的支持
                setupState[k] = v
              } else {
                console.error('不存在')
              }
            },
          })

          // 在这里调用created钩子
          created && created.call(renderContext)

          effect(
            () => {
              // 调用render函数时，将其this设置为state
              // 从而使得render函数内部可以通过this来访问自身状态数据
              // 同时将state作为render函数的第一个参数进行传递
              // 执行渲染函数 获取组件要渲染的内容，即render函数返回的虚拟DOM
              const subTree = render.call(renderContext, renderContext)
              if (!instance.isMounted) {
                // 在这里调用beforeMount钩子
                beforeMount && beforeMount.call(renderContext)
                // 最后调用patch函数来挂载组件所描述的内容，即subTree
                patch(null, subTree, container, anchor)
                // 将实例的isMounted设置为true,这样当更新发生时就不会再次进行挂载操作
                // 而是会执行更新
                instance.isMounted = true
                // 在这里调用mounted
                mounted && mounted.call(renderContext)
              } else {
                // 在这里调用beforeUpdate
                beforeUpdate && beforeUpdate.call(renderContext)
                // isMounted为true的时候说明已经被挂载，只需要完成自更新即可
                // 所以在调用patch函数时，第一个参数就是上一次渲染的子树(虚拟DOM)
                // 意思是，使用新的子树与上一次渲染的子树进行打补丁操作
                patch(instance.subTree, subTree, container, anchor)
                // 在这里调用updated钩子
                updated && updated.call(renderContext)
              }
              // 更新组件实例的子树
              instance.subTree = subTree
              console.log(instance.isMounted)
              console.log(instance.subTree)
            },
            {
              // 指定该副作用函数的调度器为queueJob即可
              scheduler: queueJob,
            }
          )
        }

        // resolveProps函数用来解析组件props和 attrs数据
        function resolveProps(options, propsData) {
          const props = {}
          const attrs = {}
          // 循环遍历虚拟DOM自身的props
          for (const key in propsData) {
            // 如果为组件传递的props在组件自身的props选项中有定义，则被视为是合法的props
            if (key in options || key.startsWith('on')) {
              props[key] = propsData[key]
            } else {
              // 否则将其视为attrs
              attrs[key] = propsData[key]
            }
          }
          // 最后返回props和attrs
          return [props, attrs]
        }
        function patch(n1, n2, container, anchor) {
          console.log('🚀 ~ patch ~ n2:', n2)
          // 如果两个vnode的元素类型不同，直接卸载旧的，然后再向下执行，挂载新的
          if (n1 && n1.type !== n2.type) {
            unmount(n1)
            n1 = null
          }
          const { type } = n2
          if (typeof type === 'string') {
            // 如果说旧节点不存在，那么就意味着是挂载，调用mountElement方法
            if (!n1) {
              mountElement(n2, container, anchor)
            } else {
              // 如果有旧节点，那么意味着打补丁
              patchElement(n1, n2)
            }
          } else if (typeof type === 'object') {
            // n2.type的值类型是对象，那么说明是一个组件
            if (!n1) {
              // 挂载组件
              mountComponent(n2, container, anchor)
            } else {
              // 更新组件
              patchComponent(n1, n2, anchor)
            }
          } else if (type === Text) {
            // 处理其他类型的vnode
            if (!n1) {
              // 使用createTextNode创建文本节点
              const el = (n2.el = createText(n2.children))
              // 将文本节点插入到容器中
              insert(el, container)
            } else {
              const el = (n2.el = n1.el)
              if (n2.children !== n1.children) {
                setText(el, n2.children)
              }
            }
          } else if (type === Comment) {
            if (!n1) {
              // 使用createTextNode创建文本节点
              const el = (n2.el = createComment(n2.children))
              // 将文本节点插入到容器中
              insert(el, container)
            } else {
              const el = (n2.el = n1.el)
              if (n2.children !== n1.children) {
                setText(el, n2.children)
              }
            }
          } else if (type === Fragment) {
            if (!n1) {
              n2.children.forEach((c) => patch(null, c, container))
            } else {
              patchChildren(n1, n2, container)
            }
          }
        }
        function render(vnode, container) {
          if (vnode) {
            // 新vnode存在，并且旧的vnode一起传递给patch函数，进行打补丁
            patch(container._vnode, vnode, container)
          } else {
            if (container._vnode) {
              // 旧vnode存在，且新vnode不存在，说明是卸载
              // 只需要将container内的dom清空即可
              // 拿到真实DOM
              // 拿到真实DOM的父元素
              // 从父元素中移除
              unmount(container._vnode)
            }
          }
          // 把vnode存储到container的_vnode瞎，也就是后续渲染中的旧vnode
          container._vnode = vnode
        }
        return {
          render,
        }
      }

      const render2 = createRender({
        createElement(tag) {
          return document.createElement(tag)
        },
        setElementText(el, text) {
          el.textContent = text
        },
        createComment(text) {
          return document.createComment(text)
        },
        setComment(el, text) {
          el.nodeValue = text
        },
        createText(text) {
          return document.createTextNode(text)
        },
        setText(el, text) {
          el.nodeValue = text
        },
        insert(el, parent, anchor = null) {
          parent.insertBefore(el, anchor)
        },
        patchProps(el, key, prevValue, nextValue) {
          if (/^on/.test(key)) {
            // 定义el._vei为一个对象，存在事件名称到事件处理函数的映射
            const invokers = el._vei || (el._vei = {})
            let invoker = invokers[key]
            // 根据属性名称得到对应的事件名称 例如 onClick -> click
            const name = key.slice(2).toLowerCase()
            if (nextValue) {
              if (!invoker) {
                // 如果没有invoker，则伪造一个invoker缓存到el._vei中
                // vei是 vue event invoker的首字母缩写
                // 将事件处理函数缓存到el._vei[key]下，避免覆盖
                invoker = el._vei[key] = (e) => {
                  // 如果事件发生的时间早于事件处理函数绑定的时间，则不执行事件处理函数
                  if (e.timeStamp < invoker.attached)
                    if (Array.isArray(invoker.value)) {
                      // 如果invoker.value是一个数组，那么遍历它并逐个调用事件处理函数
                      invoker.value.forEach((fn) => fn(e))
                    } else {
                      invoker.value(e)
                    }
                }
                // 将真正的事件处理函数nextValue赋值给invoker.value
                invoker.value = nextValue
                // 添加invoker.attached属性，存储事件处理函数被绑定的事件
                invoker.attached = performance.now()
                // 绑定invoker作为事件处理函数
                el.addEventListener(name, invoker)
              } else {
                // 如果invoker存在，意味着更新，并且只需要更新invoker.value的值
                invoker.value = nextValue
              }
            } else if (invoker) {
              // 新的事件绑定函数不存在，且之前绑定的invoker存在，则移除上一次的事件回调函数
              el.removeEventListener(name, invoker)
            }
          } else if (key === 'class') {
            el.className = nextValue || ''
          } else if (shouldSetAsProps(el, key, nextValue)) {
            const type = typeof el[key]
            if (type === 'boolean' && nextValue === '') {
              el[key] = true
            } else {
              el[key] = nextValue
            }
          } else {
            el.setAttribute(key, nextValue)
          }
        },
      })

      const MyComponent = {
        // 组件名称 可选的
        name: 'MyComponent',
        data() {
          return {
            foo: 'hello world',
          }
        },
        props: {
          title: String,
        },
        beforeCreate() {
          console.log('beforeCreate')
        },
        created() {
          console.log('created')
        },
        beforeMount() {
          console.log('beforeMount')
        },
        mounted() {
          console.log('mounted', this.title)
        },
        updated() {
          console.log('updated')
        },
        // setup返回一个对象
        setup(props, { emit }) {
          const count = ref(0)
          // 发射change事件，并传递给事件处理函数两个参数
          emit('change', 1, 2)
          return {
            count,
          }
        },
        // setup返回一个函数
        // setup() {
        //   return () => {
        //     return {
        //       type: 'div',
        //       children: `hello`,
        //     }
        //   }
        // },
        // 组件的渲染函数，它的返回值必须为虚拟DOM
        render(state) {
          // 返回一个虚拟DOM
          return {
            type: 'div',
            children: `count的值是：${this.count.value}`, //在渲染函数内使用组件内部的状态
          }
        },
      }
      const handler = function(...payload) {
        console.log('payload', ...payload)
      }
      let CompVNode
      CompVNode = {
        type: MyComponent,
        props: {
          title: 'A big Title',
          onChange: handler
        },
      }
      render2.render(CompVNode, document.querySelector('#app'))
      // setTimeout(() => {
      //   CompVNode = {
      //     type: MyComponent,
      //     props: {
      //       title: 'A small Title',
      //     },
      //   }
      //   render2.render(CompVNode, document.querySelector('#app'))
      // }, 2000)
    </script>
  </body>
</html>
